package umg.os.devices.filesystem.basic;

import java.io.IOException;
import java.util.ArrayList;
import java.util.InputMismatchException;
import java.util.List;

import umg.os.devices.DataManipulationUtilities;
import umg.os.devices.filesystem.basic.HDDHandler.VolumeUnavailableException;

public class BasicFileSystem {

	private HDDHandler driver;
	private byte[] sectorsData; 

	public static final short 
	SECTOR_SIZE = 512,
	USEFUL_SECTOR_SIZE = SECTOR_SIZE-3,
	LOGICAL_ADDRESS_OFFSET = (short) (((HDDHandler.DISK_SIZE / SECTOR_SIZE) / 8) / SECTOR_SIZE);

	public BasicFileSystem(HDDHandler driver) throws IOException, VolumeUnavailableException
	{
		this.driver = driver;
		loadSectorsMetadata();
	}

	public void clearSectorPointer(int sector) throws IOException, VolumeUnavailableException {
		int memoryAddress = BasicFileSystem.castLogicalToRealAddress(sector) + USEFUL_SECTOR_SIZE;
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] pointer = {(byte)0x00, (byte)0x00, (byte)0x00};
		driver.saveData(memoryAddress, pointer);
		if (closeAtTheEnd) {
			driver.close();
		}	
	}

	public void clearPointerOfLinkedSectors(int sector) throws IOException, VolumeUnavailableException {
		List<Integer> linkedSectors = getLinkedSectors(sector);
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		for (Integer linkedSector : linkedSectors) {
			clearSectorPointer(linkedSector.intValue());
		}
		if (closeAtTheEnd) {
			driver.close();
		}
	}
	
	public boolean setSectorPointer(int sector, int nextPointer) throws DataOversizedException, IOException, VolumeUnavailableException {
		int memoryAddress = BasicFileSystem.castLogicalToRealAddress(sector) + USEFUL_SECTOR_SIZE,
				nextPointerRealAddress = BasicFileSystem.castLogicalToRealAddress(nextPointer);
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] pointer;
		try {
			pointer = DataManipulationUtilities.intTo3BytesArray(nextPointerRealAddress);
		} catch (InputMismatchException ime) {
			throw new DataOversizedException("The nextPointer is too big!...", ime);
		}
		driver.saveData(memoryAddress, pointer);
		if (closeAtTheEnd) {
			driver.close();
		}
		return true;	
	}

	public void writeSector(int sector, byte[] data) throws DataOversizedException, IOException, VolumeUnavailableException {
		if (data.length > USEFUL_SECTOR_SIZE) {
			throw new DataOversizedException("The max size of the data to store in a sector is "+USEFUL_SECTOR_SIZE+" you tryed to save "+data.length);
		}
		int memoryAddress = BasicFileSystem.castLogicalToRealAddress(sector);
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		driver.saveData(memoryAddress, data);
		if (closeAtTheEnd) {
			driver.close();
		}	
	}
	
	public void writeLinkingSectors(byte[][] data,int[]sectors) throws DataOversizedException, IOException, VolumeUnavailableException {
		if (data.length != sectors.length) {
			throw new InputMismatchException("The number of data sets must be equal to the number of address");
		}
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		final int lastSectorToLink = sectors.length - 1;
		setSectorPointer(sectors[lastSectorToLink], 0);
		for (int sectorsWritten = 0; sectorsWritten < sectors.length; sectorsWritten++) { 
			driver.saveData(BasicFileSystem.castLogicalToRealAddress(sectors[sectorsWritten]), data[sectorsWritten]);
			if (sectorsWritten < lastSectorToLink) {
				setSectorPointer(sectors[sectorsWritten], sectors[sectorsWritten + 1]);
			}
		}
		if (closeAtTheEnd) {
			driver.close();
		}
	}
	
	public void writeInSectors(byte[][] data,int[]sectors) throws DataOversizedException, IOException, VolumeUnavailableException {
		if (data.length != sectors.length) {
			throw new InputMismatchException("The number of data sets must be equal to the number of address");
		}
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		for (int i = 0; i < data.length; i++) {
			writeSector(sectors[i], data[i]);
		}
		if (closeAtTheEnd) {
			driver.close();
		}
	}
	
	public void writeInLinkedSectors(int firstSector, byte[] data) throws DataOversizedException, IOException, VolumeUnavailableException {
		List<Integer> linkedSectors = getLinkedSectors(firstSector);
		if ((linkedSectors.size() * USEFUL_SECTOR_SIZE) < data.length) {
			throw new DataOversizedException("The linked sectors off this file are not enough to save the new data");
		}
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] sectorToWrite = new byte[USEFUL_SECTOR_SIZE];
		int sectorsWritten = 0;
		for (Integer sector : linkedSectors) {
			System.arraycopy(data, sectorsWritten * USEFUL_SECTOR_SIZE, sectorToWrite, 0, USEFUL_SECTOR_SIZE);
			writeSector(sector.intValue(), sectorToWrite);
			sectorsWritten++;
		}
		if (closeAtTheEnd) {
			driver.close();
		}
	}
	
	public byte[] readSector(int sector) throws IOException, VolumeUnavailableException {
		int memoryAddress = BasicFileSystem.castLogicalToRealAddress(sector);
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] dataRead = driver.readData(memoryAddress, USEFUL_SECTOR_SIZE);
		if (closeAtTheEnd) {
			driver.close();
		}
		return dataRead;	
	}
	
	public byte[] readContinuouslyFromSectors(int[]sectors) throws IOException, VolumeUnavailableException {
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] dataRead = new byte[sectors.length * USEFUL_SECTOR_SIZE];
		for (int i = 0; i < sectors.length; i++) {
			/*System.arraycopy(
					driver.readData(BasicFileSystem.castLogicalToRealAddress(sectors[i]), USEFUL_SECTOR_SIZE), 
					0, 
					dataRead, 
					(i * USEFUL_SECTOR_SIZE), 
					USEFUL_SECTOR_SIZE);*/
			System.arraycopy(
					readSector(sectors[i]), 
					0, 
					dataRead, 
					(i * USEFUL_SECTOR_SIZE), 
					USEFUL_SECTOR_SIZE);
		}
		if (closeAtTheEnd) {
			driver.close();
		}
		return dataRead;
	}
	
	public byte[] readContinuouslyFromLinkedSectors(int firstSector) throws IOException, VolumeUnavailableException {
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		List<Integer> sectors = getLinkedSectors(firstSector);
		byte[] dataRead = new byte[sectors.size() * USEFUL_SECTOR_SIZE];
		int sectorsWritten = 0;
		for (Integer sector : sectors) {
			System.arraycopy(
					readSector(sector.intValue()), 
					0, 
					dataRead, 
					(sectorsWritten * USEFUL_SECTOR_SIZE), 
					USEFUL_SECTOR_SIZE);
			sectorsWritten++;
		}
		if (closeAtTheEnd) {
			driver.close();
		}
		return dataRead;
	}
	
	protected byte[][] readFromSectors(int[]sectors) throws IOException, VolumeUnavailableException {
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[][] dataRead = new byte[sectors.length * USEFUL_SECTOR_SIZE][USEFUL_SECTOR_SIZE];
		for (int i = 0; i < sectors.length; i++) {
			dataRead[i] = driver.readData(BasicFileSystem.castLogicalToRealAddress(sectors[i]), USEFUL_SECTOR_SIZE);
		}
		if (closeAtTheEnd) {
			driver.close();
		}
		return dataRead;
	}

	public void freeSector(int sector) throws IOException, VolumeUnavailableException {
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] data = {BasicFileSystem.getBitMaskInPosition(((sector + LOGICAL_ADDRESS_OFFSET) % 8))};
		driver.saveData(((sector + LOGICAL_ADDRESS_OFFSET) / 8), data);
		if (closeAtTheEnd) {
			driver.close();
		}
	}

	public int reserveSector() throws IOException, VolumeUnavailableException {
		int byteInDiskToUpdate, sector = LOGICAL_ADDRESS_OFFSET;
		while(sectorsData[sector] == (byte)0xFF) {
			sector++;
		}
		byte byteOfInteres = sectorsData[sector];
		byteInDiskToUpdate = sector;
		sector *= 8;
		while (byteOfInteres >= 0x80) {
			byteOfInteres = (byte) (byteOfInteres << 1);
			sector++;
		}
		sectorsData[byteInDiskToUpdate] = byteOfInteres;
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] dataToSave = {byteOfInteres}; 
		driver.saveData(byteInDiskToUpdate, dataToSave);
		clearSectorPointer(sector);
		if (closeAtTheEnd) {
			driver.close();
		}
		return sector - LOGICAL_ADDRESS_OFFSET;
	}

	public void saveLogicalMetadata(byte[] data) throws IOException, VolumeUnavailableException, DataOversizedException {
		if (data.length % USEFUL_SECTOR_SIZE != 0) {
			throw new InputMismatchException("The logical methadata must fit perfectly in the sectors");
		}
		byte[] dataToBeWritten = new byte[USEFUL_SECTOR_SIZE];
		List<Integer> logicalMetadataSectors = getLinkedSectors(0); //The metadata is in the logical 0 sector
		int requiredSectors = data.length/USEFUL_SECTOR_SIZE;
		while (logicalMetadataSectors.size() < requiredSectors) {
			logicalMetadataSectors.add(new Integer(reserveSector()));
		}
		int sectorsSaved = 0;
		for (int offset = 0; offset < data.length; offset += USEFUL_SECTOR_SIZE) {
			System.arraycopy(data, offset, dataToBeWritten, 0, USEFUL_SECTOR_SIZE);
			/*if ((sectorToBeWritten = logicalMetadataSectors.get(sectorsSaved)) == null) {
				sectorToBeWritten = reserveSector();
			}*/
			writeSector(logicalMetadataSectors.get(sectorsSaved).intValue(), dataToBeWritten);
			if (sectorsSaved < (requiredSectors - 1)) {
				setSectorPointer(logicalMetadataSectors.get(sectorsSaved).intValue(), logicalMetadataSectors.get(sectorsSaved + 1));
			}
			sectorsSaved++;
		}
		while(logicalMetadataSectors.size() > sectorsSaved) {
			clearSectorPointer(logicalMetadataSectors.get(sectorsSaved));
			sectorsSaved++;
		}
	}
	
	public void freeSectors(int sectors[]) throws IOException, VolumeUnavailableException {
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		for (int i : sectors) {
			byte[] data = {BasicFileSystem.getBitMaskInPosition(((i + LOGICAL_ADDRESS_OFFSET) % 8))};
			driver.saveData(((i + LOGICAL_ADDRESS_OFFSET) / 8), data);
		}
		if (closeAtTheEnd) {
			driver.close();
		}
	}

	public List<Integer> getLinkedSectors(int sector) throws IOException, VolumeUnavailableException
	{
		ArrayList<Integer> castedAddress = new ArrayList<Integer>();
		for (Integer i : driver.getLinkedAddress(BasicFileSystem.castLogicalToRealAddress(sector))) {
			castedAddress.add(new Integer(castRealToLogicalAddress(i)));
		}
		return castedAddress;
	}

	public static int castLogicalToRealAddress(int sector)
	{
		return (int) ((sector + LOGICAL_ADDRESS_OFFSET) * SECTOR_SIZE);
	}

	public static int castRealToLogicalAddress(int address)
	{
		return (int) ((address / SECTOR_SIZE) - LOGICAL_ADDRESS_OFFSET);
	}	

	public static long getSizeInDiskFor(long amountOfBytes) {
		if (amountOfBytes < 0) {
			throw new InputMismatchException("The size to calculate cannot be negative!");
		}
		if (amountOfBytes == 0) {
			return SECTOR_SIZE;
		} else {
			return (((amountOfBytes / USEFUL_SECTOR_SIZE)) * SECTOR_SIZE);
		}
	}
	
 	private void loadSectorsMetadata() throws IOException, VolumeUnavailableException {
		int sizeOfSectorsData = (int) ((HDDHandler.DISK_SIZE / SECTOR_SIZE) / 8);
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		sectorsData = driver.readData(0, sizeOfSectorsData);
		if (closeAtTheEnd) {
			driver.close();
		}
	}

	public byte[] getLogicalMetadata() throws IOException, VolumeUnavailableException {
		int metadataInitialAddress = (int) ((HDDHandler.DISK_SIZE / SECTOR_SIZE) / 8);
		boolean closeAtTheEnd = false;
		if (!driver.isOpen()) {
			closeAtTheEnd = true;
			driver.open();
		}
		byte[] dataRead = driver.readFromLinkedSectors(metadataInitialAddress);
		if (closeAtTheEnd) {
			driver.close();
		}
		return dataRead;
	}

	/*protected byte[] readFromLinkedSectors(int initialAddress) {
		if(initialAddress%512!=0) {
			throw new InputMismatchException("Invalid memmory address");
		}
		List<Integer> fileAddress = driver.getLinkedAddress(initialAddress);
		byte[] data = new byte[fileAddress.size()*BasicFileSystem.USEFUL_SECTOR_SIZE];
		try (RandomAccessFile raf= new RandomAccessFile(hd.toFile(),"rwd")) {
			int sectorsRead = 0;
			for(Integer i : fileAddress)
			{
				raf.seek(i);
				raf.read(data, sectorsRead * BasicFileSystem.USEFUL_SECTOR_SIZE, BasicFileSystem.USEFUL_SECTOR_SIZE);
				sectorsRead++;
			}
		} 
		catch (FileNotFoundException e) 
		{
			e.printStackTrace();
			System.err.println(e.getMessage());
			return null;
			
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
			System.err.println(e.getMessage());
			return null;
		}
		return data;
	}*/
	
	/*protected List<Integer> getLinkedAddress(int address){
		if(address%512!=0) {
			throw new InputMismatchException("Invalid memmory address");
		}
		byte[] nextAddressAsBytes = new byte[3];
		ArrayList<Integer> linkedAddress = new ArrayList<Integer>();
		try (RandomAccessFile raf= new RandomAccessFile(hd.toFile(),"rwd")) {
			do 
			{
				linkedAddress.add(address);
				raf.seek(address+BasicFileSystem.USEFUL_SECTOR_SIZE);
				raf.read(nextAddressAsBytes, 0, 3);
			}while((address = DataManipulationUtilities.intFrom3bytes(nextAddressAsBytes)) != 0);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return linkedAddress;
	}*/
	
	public static void formatDisc(HDDHandler volumeToFormat) throws IOException, VolumeUnavailableException {
		System.out.println("Formating Disk...");
		int 
		sizeOfSectorsData = (int) ((HDDHandler.DISK_SIZE/SECTOR_SIZE) / 8), 
		occupiedSectors = sizeOfSectorsData / SECTOR_SIZE;
		byte[] sectorsData = new byte[(int) ((HDDHandler.DISK_SIZE/SECTOR_SIZE) / 8)];
		for (int i = 0; i < (occupiedSectors / 8); i++) {
			sectorsData[i] = (byte)0xFF;
		}
		byte lastByte = (byte) 0x00;
		for (int i = 0; i < (occupiedSectors % 8); i++) {
			lastByte++;
			lastByte = (byte) (lastByte << 1);
		}
		lastByte = (byte) (lastByte << (8 - (occupiedSectors % 8)));
		sectorsData[(occupiedSectors / 8)] = lastByte;
		if (!volumeToFormat.isOpen()) {
			volumeToFormat.open();
		}
		volumeToFormat.saveData(0, sectorsData);
		volumeToFormat.close();
		System.out.println("Format complete");
	}

	public static byte getBitMaskInPosition(int position) {
		return (byte) (((byte) 1) << position);
	}
	
	@SuppressWarnings("serial")
	public class DataOversizedException extends Exception {

		private DataOversizedException(String message) {
			super(message);
		}

		private DataOversizedException(String message, Throwable reason) {
			super(message, reason);
		}

	}

}
